/*
 *  levelUpGame - GameController.cpp
 *  Copyright 2011 W. Reckman. All rights reserved.
 */
#include <math.h>
#include "LevelUpApp.h"
#include "ofUtils.h"
#include "Settings.h"
#include "Utils.h"
#include "GameModel.h"
#include "Pickup.h"

/* TODO: these includes need refactoring (as should the whole project regarding header deps) */
#include "InputController.h"
#include "RemoteIO.h"
#include "Player.h"


const int GameController::END_SEQUENCE_TOTAL_STAGES = 2;

//--------------------------------------------------------------
GameController::GameController( LevelUpApp& appRef_ )
: s( Settings::getInstance() ), appRef( appRef_ ), game( appRef.getGame() ),
  collisionHandler( appRef_ ), gameEventHandler( appRef_ ),
  sendBalanceChangedEventFlag( false ), sendGameStateEventFlag( false ),
  sendPrecountMovementEventForPlayer( -1 ), lastPlayerIndexAdded( -1 )
{}

//--------------------------------------------------------------
GameController::~GameController() {
	for ( int i = 0; i < queuedPlayers.size(); ++i ) {
		delete queuedPlayers[i].topImagePath;
		delete queuedPlayers[i].bottomImagePath;
	}
	queuedPlayers.clear();
}


//--------------------------------------------------------------
void GameController::setup() {
	/* init OSC sender */
	
	photoboothHostName = *s.getValue( "photobooth_hostname", "localhost" );
	photoboothPort = ofToInt( *s.getValue( "photobooth_port", "8766" ) );
	
	oscSender.setup( photoboothHostName, photoboothPort );
	
	/* init game related stuff */
	
	collisionHandler.setup();
	gameEventHandler.setup();
	ofAddListener( onCollisionDetected, &collisionHandler, &CollisionHandler::onCollisionDetected );
	ofAddListener( gameEvent, &gameEventHandler, &GameEventHandler::onGameEvent );
	
	argsGameEvent.eventType = EventArgsGame::GE_GAME;
	argsGameEvent.eventSubtype = EventArgsGame::GES_GAME_INIT;
	ofNotifyEvent( gameEvent, argsGameEvent, this );
}

//--------------------------------------------------------------
void GameController::update() {
	if ( game.isPaused() ) return;
	
	float lastPlayTimeLeft = game.getPlayTimeLeft();
	float lastPrecountTimeLeft = game.getPrecountTimeLeft();
	
	argsGameEvent.eventType = EventArgsGame::GE_NEW_FRAME;
	ofNotifyEvent( gameEvent, argsGameEvent, this );
	
	if ( game.getState() == GS_END_SEQUENCE ) {
		if ( endSequenceEndTime != -1 && game.getTime() >= endSequenceEndTime ) {
			endSequenceEndTime = -1;
			argsGameEvent.eventType = EventArgsGame::GE_GAME;
			argsGameEvent.eventSubtype = EventArgsGame::GES_GAME_STATE;
			
			if ( endSequenceStage < END_SEQUENCE_TOTAL_STAGES ) {
				endSequenceStage++;
				argsGameEvent.prevState = GS_END_SEQUENCE;
				argsGameEvent.currState = GS_END_SEQUENCE;
				argsGameEvent.intVal = endSequenceStage;
			} else {
				argsGameEvent.prevState = GS_END_SEQUENCE;
				argsGameEvent.currState = GS_END;
			}
			ofNotifyEvent( gameEvent, argsGameEvent, this );
		}
	}
	
	game.update();
	
	if ( floorf( lastPrecountTimeLeft ) > floorf( game.getPrecountTimeLeft() ) ) {
		argsGameEvent.eventType = EventArgsGame::GE_GAME;
		argsGameEvent.eventSubtype = EventArgsGame::GES_GAME_PRECOUNT_TICK;
		argsGameEvent.intVal = floorf( game.getPrecountTimeLeft() );
		ofNotifyEvent( gameEvent, argsGameEvent, this );
	}
	
	if ( lastPrecountTimeLeft > 0.0f && game.getPrecountTimeLeft() <= 0.0f ) {
		changeGameState( GS_PLAYING );
	}
	
	if ( lastPlayTimeLeft > 0.0f && game.getPlayTimeLeft() <= 0.0f ) {
		changeGameState( GS_END_SEQUENCE );
	}
	
	
	//some housekeeping (remove destroyed pickups)
	GameModel::vec_PickupP& pickups = game.getPickups();
	GameModel::vec_PickupP::iterator itpi = pickups.begin();
	
	while ( itpi != pickups.end() ) {
		Pickup* pickup = *itpi;
		if ( pickup->getState() == Pickup::PST_DESTROYED ) {
			argsGameEvent.eventType = EventArgsGame::GE_ODESTROYED;
			argsGameEvent.objType = EventArgsGame::GOT_PICKUP;
			argsGameEvent.obj.pickup = pickup;
			ofNotifyEvent( gameEvent, argsGameEvent, this );
			
			delete pickup;
			itpi = pickups.erase( itpi );
		} else {
			++itpi;
		}
	}
	
	game.p0Colliding = false; //TEMP
	checkCollisions();
	
	if ( sendGameStateEventFlag ) {
		sendGameStateEventFlag = false;
		argsGameEvent.eventType = EventArgsGame::GE_GAME;
		argsGameEvent.eventSubtype = EventArgsGame::GES_GAME_STATE;
		argsGameEvent.prevState = lastGameState;
		argsGameEvent.currState = game.getState();
		if ( game.getState() == GS_END_SEQUENCE ) argsGameEvent.intVal = 1;
		ofNotifyEvent( gameEvent, argsGameEvent, this );
	}
	
	if ( sendPrecountMovementEventForPlayer >= 0 ) {
		argsGameEvent.eventType = EventArgsGame::GE_PLAYER;
		argsGameEvent.eventSubtype = EventArgsGame::GES_PLAYER_PRECOUNT_MOVE_ATTEMPT;
		argsGameEvent.objType = EventArgsGame::GOT_PLAYER;
		argsGameEvent.obj.player = game.getPlayers()[sendPrecountMovementEventForPlayer];
		ofNotifyEvent( gameEvent, argsGameEvent, this );
		sendPrecountMovementEventForPlayer = -1;
	}
	
	if ( sendBalanceChangedEventFlag ) {
		sendBalanceChangedEventFlag = false;
		argsGameEvent.eventType = EventArgsGame::GE_GAME;
		argsGameEvent.eventSubtype = EventArgsGame::GES_GAME_BALANCE_CHANGED;
		argsGameEvent.floatVal = game.getBalanceMeter().getBalanceFloat();
		ofNotifyEvent( gameEvent, argsGameEvent, this );
	}
}

//--------------------------------------------------------------
int GameController::getQueuedPlayerCount() const { return queuedPlayers.size(); }

//--------------------------------------------------------------
int GameController::getEndSequenceStage() const { return endSequenceStage; }
float GameController::getEndSequenceTimer() const { return endSequenceEndTime; }
void GameController::setEndSequenceStage( int stage_ ) { endSequenceStage = stage_; }
void GameController::setEndSequenceTimer( float seconds_ ) { endSequenceEndTime = game.getTime() + seconds_; }

//--------------------------------------------------------------
void GameController::requestNewPlayersFromPhotobooth() const {
	static int count = 1;
	ofLog( OF_LOG_NOTICE, "(GameController.cpp) requesting new players from photobooth (request #%i)", count );
	ofxOscMessage msg;
	msg.setAddress( "/levelUp/game" );
	msg.addStringArg( "sendNewPlayers" );
	count++;
	oscSender.sendMessage( msg );
}

//--------------------------------------------------------------
void GameController::changeGameState( GAME_STATE newState_, bool sendEvent_ ) {
	lastGameState = game.getState();
	game.setState( newState_ );
	
	if ( sendEvent_ ) sendGameStateEventFlag = true;
}

//--------------------------------------------------------------
void GameController::sendBalanceChangedEvent() const {
	sendBalanceChangedEventFlag = true;
}

//--------------------------------------------------------------
bool GameController::createPlayerFromQueue( int queueIndex_ ) {
int orgQueueIndex = queueIndex_; //TEMP
	if ( queueIndex_ == -1 ) {
		for ( int i = 0; i < queuedPlayers.size(); ++i ) {
			if ( queuedPlayers[i].topImagePath && queuedPlayers[i].bottomImagePath ) {
				queueIndex_ = i;
				break;
			}
		}
	}
ofLog( OF_LOG_NOTICE, "(GameController.cpp) creating player from queue index %i (arg was %i)", queueIndex_, orgQueueIndex ); //TEMP
	
	if ( queueIndex_ >= queuedPlayers.size() ) {
		ofLog( OF_LOG_ERROR, "(GameController.cpp) queued player index %i out of range (max=%i)", queueIndex_, queuedPlayers.size() - 1 );
		return false;
	}
	
	int numPlayers = game.getPlayers().size();
	
	Player* p = game.createPlayer( queuedPlayers[queueIndex_] );
	if ( p ) {
		queuedPlayers.erase( queuedPlayers.begin() + queueIndex_ );
		
		argsGameEvent.eventType = EventArgsGame::GE_OCREATED;
		argsGameEvent.objType = EventArgsGame::GOT_PLAYER;
		argsGameEvent.obj.player = p;
		ofNotifyEvent( gameEvent, argsGameEvent, this );
		return true;
	} else {
		ofLog( OF_LOG_ERROR, "(GameController.cpp) could not create player with number %i (top img = '%s')", numPlayers, queuedPlayers[queueIndex_].topImagePath->c_str() );
		return false;
	}
}


/*******************
 * EVENT CALLBACKS *
 *******************/

//--------------------------------------------------------------
void GameController::onPlayerInput( EventArgsJoystick& ev_ ) {
	GameModel::vec_PlayerP& players = game.getPlayers();
	if ( ev_.joystickNum < players.size() ) {
		if ( game.getState() == GS_PRECOUNT ) sendPrecountMovementEventForPlayer = ev_.joystickNum;
		players[ev_.joystickNum]->setDirection( ev_.joystickDirection );
		//ofLog( OF_LOG_WARNING, "received event for player %i, dir=%i", ev_.joystickNum, ev_.joystickDirection );
	}
}

//--------------------------------------------------------------
void GameController::onNewPhotoboothFile( EventArgsPhotoboothFile& ev_ ) {
	static int colorIdx = 0; //TEMP yehyeh
	GameModel::PlayerInfo qp;
	bool rv;
	const string& filename = ev_.filePath;
	
	ofLog( OF_LOG_NOTICE, "(GameController.cpp) received new file: %s", filename.c_str() );
	
	rv = parseFilename( filename, qp );
	if ( rv ) {
		int foundQp = -1;
		for ( int i = 0; i < queuedPlayers.size(); ++i ) {
			if ( queuedPlayersEqual( qp, queuedPlayers[i] ) ) {
				foundQp = i;
				break;
			}
		}
		
		if ( foundQp >= 0 ) {
			if ( ( qp.topImagePath && ! queuedPlayers[foundQp].topImagePath ) || ( qp.bottomImagePath && ! queuedPlayers[foundQp].bottomImagePath ) ) {
				if ( qp.topImagePath ) queuedPlayers[foundQp].topImagePath = qp.topImagePath;
				else queuedPlayers[foundQp].bottomImagePath = qp.bottomImagePath;
				ofLog( OF_LOG_NOTICE, "(GameController.cpp) completing queued player (top img = '%s'", queuedPlayers[foundQp].topImagePath->c_str() );
				
				argsGameEvent.eventType = EventArgsGame::GE_PLAYER_QUEUED;
				argsGameEvent.intVal = foundQp;
				ofNotifyEvent( gameEvent, argsGameEvent, this );
			} else {
				ofLog( OF_LOG_WARNING, "(GameController.cpp) found duplicate/incorrect/missing file _NOT_ matching with counterpart...add more debug logging for details (sorry)" );
			}
		} else {
			queuedPlayers.push_back( qp );
			ofLog( OF_LOG_NOTICE, "(GameController.cpp) adding new queued player (waiting for counterpart)" );
		}
	} else {
		ofLog( OF_LOG_WARNING, "(GameController.cpp) ignoring received player image with improperly formatted name ('%s')", filename.c_str() );
	}
}

/*********************
 * PRIVATE FUNCTIONS *
 *********************/

//--------------------------------------------------------------
//NOTE: filename format is MM-DD-hh-mm-ss-ccc-iii-p_{Top,Bottom}.png
//NOTE: slashes are not allowed in the filename
bool GameController::parseFilename( const string& name, GameModel::PlayerInfo& qp ) {
	static const int fieldCount = 8;
	static const char splitChar = '-';
	
	string stripped = name.substr( 0, name.rfind( '.' ) );
	stripped = stripped.substr( stripped.rfind( '/' ) + 1 );
	
	int topIdx = stripped.find( "_Top" );
	int bottomIdx = stripped.find( "_Bottom" );
	
	if ( topIdx != string::npos ) {
		stripped = stripped.substr( 0, topIdx );
	} else if ( bottomIdx != string::npos ) {
		stripped = stripped.substr( 0, bottomIdx );
	} else {
		return false; //EXIT point
	}
	
	std::vector<string>* tokens = tokenize( stripped, splitChar );
	
	if ( tokens->size() != fieldCount ) return false; //EXIT point
	
	for ( int i = 0; i < tokens->size(); ++i ) {
		int v = ofToInt( (*tokens)[i] );
		switch ( i ) {
			case 0: qp.month = v; break;
			case 1: qp.day = v; break;
			case 2: qp.hours = v; break;
			case 3: qp.minutes = v; break;
			case 4: qp.seconds = v; break;
			case 5: qp.cultureTgt = v; break;
			case 6: qp.industryTgt = v; break;
			case 7: qp.id = v - 1; break;
		}
	}
	
	if ( qp.topImagePath != 0 ) delete qp.topImagePath;
	if ( qp.bottomImagePath != 0 ) delete qp.bottomImagePath;
	qp.topImagePath = qp.bottomImagePath = 0;
	
	qp.goodPickups = qp.badPickups = 0;
	qp.color = game.getPlayerColor( qp.id );
	
	if ( topIdx != string::npos ) qp.topImagePath = new string( name );
	else qp.bottomImagePath = new string( name );
	
	return true;
}

//--------------------------------------------------------------
void GameController::checkCollisions() {
	GameModel::vec_PlayerP& players = game.getPlayers();
	GameModel::vec_PickupP& pickups = game.getPickups();
	Ghost* cultGhost = game.getCultureGhost();
	Ghost* indGhost = game.getIndustryGhost();
	
	//--check collisions between ghosts and pickups
	GameModel::vec_PickupP::iterator itpi = pickups.begin();
	for ( itpi = pickups.begin(); itpi != pickups.end(); ++itpi ) {
		Pickup* pickup = *itpi;
		argsCollision.co1Type = EventArgsCollision::COT_PICKUP;
		argsCollision.o1.pickup = pickup;
		
		if ( cultGhost && pickup->collidesWith( *cultGhost ) ) {
			argsCollision.co2Type = EventArgsCollision::COT_GHOST;
			argsCollision.o2.ghost = cultGhost;
			ofNotifyEvent( onCollisionDetected, argsCollision, this );
		}
		
		if ( indGhost && pickup->collidesWith( *indGhost ) ) {
			argsCollision.co2Type = EventArgsCollision::COT_GHOST;
			argsCollision.o2.ghost = indGhost;
			ofNotifyEvent( onCollisionDetected, argsCollision, this );
		}
	}
	
	
	for ( int i = 0; i < players.size(); i++ ) {
		Player* player = players[i];
		argsCollision.co1Type = EventArgsCollision::COT_PLAYER;
		argsCollision.o1.player = player;
		
		//--check collisions with other players
		for ( int j = i + 1; j < players.size(); j++ ) {
			Player* other = players[j];
			
			if ( player->collidesWith( *other ) ) {
				argsCollision.co2Type = EventArgsCollision::COT_PLAYER;
				argsCollision.o2.player = other;
				ofNotifyEvent( onCollisionDetected, argsCollision, this );
			}
		}
		
		//--check collisions with ghosts
		if ( cultGhost && player->collidesWith( *cultGhost ) ) {
			argsCollision.co2Type = EventArgsCollision::COT_GHOST;
			argsCollision.o2.ghost = cultGhost;
			ofNotifyEvent( onCollisionDetected, argsCollision, this );
		}
		
		if ( indGhost && player->collidesWith( *indGhost ) ) {
			argsCollision.co2Type = EventArgsCollision::COT_GHOST;
			argsCollision.o2.ghost = indGhost;
			ofNotifyEvent( onCollisionDetected, argsCollision, this );
		}
		
		//--check collisions with pickups
		for ( itpi = pickups.begin(); itpi != pickups.end(); ++itpi ) {
			Pickup* pickup = *itpi;
			
			if ( player->collidesWith( *pickup ) ) {
				argsCollision.co2Type = EventArgsCollision::COT_PICKUP;
				argsCollision.o2.pickup = pickup;
				ofNotifyEvent( onCollisionDetected, argsCollision, this );
			}
		}
	}
}


//--------------------------------------------------------------
//TEMP: FIXME: still uses hardcoded values
void GameController::constrainPlayersToCanvas() {
	GameModel::vec_PlayerP& players = game.getPlayers();
	ofPoint boundingBoxMin(0, 0);
	ofPoint boundingBoxMax( 1400 - 64, 1050 - 34 - 14 );
	
	for (int i = 0; i < players.size(); i++) {
		ofPoint currentPosition = players[i]->getPosition();
		
		if (currentPosition.x <= boundingBoxMin.x ) {
			players[i]->setDirection( D_STOP );
			currentPosition.x = 1;
		}
		if (currentPosition.y <= boundingBoxMin.y ) {
			players[i]->setDirection( D_STOP );
			currentPosition.y = 1;
		}
		if (currentPosition.x >= boundingBoxMax.x ) {
			players[i]->setDirection( D_STOP );
			currentPosition.x = boundingBoxMax.x - 1;
		}
		if (currentPosition.y >= boundingBoxMax.y ) {
			players[i]->setDirection( D_STOP );
			currentPosition.y = boundingBoxMax.y - 1;
		}
		
		players[i]->setPosition( currentPosition );
	}
}

//--------------------------------------------------------------
//TEMP
bool GameController::queuedPlayersEqual( const GameModel::PlayerInfo& p1, const GameModel::PlayerInfo& p2 ) const {
	return p1.month == p2.month && p1.day == p2.day && p1.hours == p2.hours &&
	p1.minutes == p2.minutes && p1.seconds == p2.seconds &&
	p1.cultureTgt == p2.cultureTgt && p1.industryTgt == p2.industryTgt;
}
